Borland Online And The Cobb Group Present:


March, 1996 - Vol. 3 No. 3

Directly editing an OWL TListBox object

by Kent Reisdorph

Last month we talked about the value of list boxes in Windows applications and showed you how to create owner-drawn list boxes using Borland's Object Windows Library (see "Owner-Drawn List Boxes in OWL"). You probably think of a list box as a means to present the user with a read-only list of data from which to make selections. However, in some cases you may want the user to be able to enter or modify data in a list box. Many applications pop up an input dialog box, but the appearance of the dialog box may break the user's concentration on the task at hand.

Alternatively, you can let the user directly edit list box entries. To do so, the user double-clicks a list box item; the selected item then becomes an edit control the user can edit directly. When the user has finished editing the item, the list box reverts to its normal state. Unfortunately, the standard Windows list box doesn't have this editing capability, so we'll need to create our own mechanism. In this article, we'll show you how to use a TEdit object as a child of a TListBox object to edit the contents of a list box.

How it works

The technique is fairly simple. You'll create a list box derived from the TListBox class. When the user double-clicks an item in the list box, you'll create a TEdit object as a child of the list box. When the user finishes editing, you'll retrieve the text from the edit control and then write the new text back to the list box.

The TListBox class provides several functions that you'll use to achieve your goal. For the most part, they're the same functions that you'd typically use in a TListBox-derived class. One function, however­­GetItemRect()­­may be new to you. You'll use this function to get the rec-tangle of the selected item, then use the rectangle's size and position to create your edit control.

Look! It's a list box! It's an edit control! It's...

Remember that controls are just specialized windows. As such, they're subject to the same rules and features as any window­­there's nothing to stop you from creating an edit control on a list box. Listing A shows the code for a test application that illustrates this technique.


Listing A: EDITLBOX.CPP

#include <owl\owlpch.h>
#pragma hdrstop
static const char TestFile="\\windows\\win.ini";
     //contents of this file are used in the demo
     //edit the path to suit

class EditListBox : public TListBox {
	public:
		EditListBox(TWindow* parent, int id,  
				    int x, int y, int w, int h);
		void EvLButtonDblClk(uint, TPoint&);
		void SetupWindow();
		void SelChanged();
		TEdit* edit;
		int editIndex;

	DECLARE_RESPONSE_TABLE(EditListBox);
};

DEFINE_RESPONSE_TABLE1(EditListBox, TListBox)
	EV_WM_LBUTTONDBLCLK,
	EV_NOTIFY_AT_CHILD(LBN_SELCHANGE, 
                       SelChanged),
END_RESPONSE_TABLE;

EditListBox::EditListBox(TWindow* parent,  
	int id,	int x, int y, int w, int h)
	: TListBox(parent, id, x, y, w, h)
{
	Attr.Style |= LBS_NOINTEGRALHEIGHT | 
		LBS_USETABSTOPS;
	Attr.Style &= ~LBS_SORT;
	edit = NULL;
}

void EditListBox::SetupWindow()
{
	TListBox::SetupWindow();
	char str[80];
	ifstream infile(TestFile);
	if (!infile) {
		MessageBox("Error Opening File");
		return;
	}
	while (!infile.eof()) {
		infile.getline(str, sizeof(str));
		AddString(str);
	}
	infile.close();
}

void EditListBox::EvLButtonDblClk(uint, TPoint&)
{
	char str[100];
	int index = GetSelIndex();
	editIndex = index;
	GetSelString(str, sizeof(str));
	TRect rect;
	GetItemRect(index, rect);
	edit = new TEdit(this, 1, "",
		rect.left, rect.top,
		rect.Width(), rect.Height());
	edit->Attr.Style &= ~WS_BORDER;
	edit->Create();
	edit->SetText(str);
	edit->SetFocus();
}

void EditListBox::SelChanged()
{
	if (edit == NULL) return;
	SetRedraw(false);
	char str[100];
	edit->GetText(str, sizeof(str));
	DeleteString(editIndex);
	InsertString(str, editIndex);
	delete edit;
	edit = NULL;
	SetRedraw(true);
}

class TestApp : public TApplication {
	public:
		TestApp() : TApplication("") {}
		void InitMainWindow()
		{
			MainWindow = new TFrameWindow 
				(0, "EditListBox Test App", 
				new EditListBox(0,0,0,0,0,0));
		}
};

int OwlMain(int /*argc*/, char* /*argv*/ [])
{
	TestApp app;
	return app.Run();
}

First, let's take a look at the class definition for the EditListBox class. We declare a constructor, a SetupWindow() function, an EvLButtonDblClk message-handling function, and a user-defined function called SelChanged(). Notice that we have two data members: edit, a pointer to a TEdit object; and editIndex, an integer variable. This integer keeps track of which list box the user is currently editing. Finally, we include a DECLARE_RESPONSE_TABLE macro to let OWL know we'll be handling some Windows messages in our list box.

The response table definition follows the class definition. The response table has only two entries. The first, EV_WM_LBUTTONDBLCLK, tells OWL that we'll be handling the left button double-click message. We'll do that via the OWL message-handling function EvLButtonDblClk(). The second response table entry is an EV_NOTIFY_AT_CHILD macro. Normally, the application sends the control notification messages to the parent of the control. The EV_NOTIFY_AT_CHILD macro tells OWL that we'll be handling a particular control notification at the control (child) level. In this case, we're telling OWL that we want to handle the LBN_SELCHANGE message at the list box level. Our application sends this message whenever the user changes the list box selection. We'll use this message as an indicator that the user has finished editing a selection and has moved to another list box item. The EV_NOTIFY_AT_CHILD macro also requires a pointer to a function that our application will call when it receives the notification. We'll use our SelChanged() function to handle the message.

Next in our application is the class's constructor. The constructor adds the LBS_NOINTEGRALHEIGHT style bit to the Attr.Style member of the TListBox object. You may remember from last month's article on owner-drawn list boxes that we add this style bit to ensure the list box covers the entire client area of the frame window. The next line removes the LBS_SORT style bit, since we don't want this example program to sort the list box. Finally, the constructor sets the TEdit* edit pointer to NULL.

Setting an otherwise unassigned pointer to NULL or 0 is a good habit to develop. Normally, you don't want to get a General Protection Fault (GPF) in your application. However, by setting a pointer to NULL, you ensure that if you inadvertently attempt to access the pointer before it's been assigned, you'll get a GPF, and the IDE debugger will conveniently point out the offending line. If you don't set the pointer to NULL, then it will contain random data. Accessing the pointer in that case may result in odd behavior that's difficult to track down. A GPF is preferable because it immediately reminds you that you're trying to use a pointer you haven't yet initialized.

Our SetupWindow() function simply loads the list box with the contents of your WIN.INI file. We call the TListBox::SetupWindow() function first­­you must call the base class SetupWindow() function first so your application will properly create the underlying window element. Following the call to TListBox::SetupWindow(), we open WIN.INI, get each line from the file, and use TListBox::AddString() to add each line to the list box. After we've read the entire file, we close it.

Where the action is

Now let's examine Listing A's EvLButtonDblClk() function, in which we create the edit control. (Some of the functions we use here are common TListBox functions; we won't spend much time on them.) The first step is to get the index of the currently selected item. We do this via TListBox::GetSelIndex(). The next line sets our member variable, editIndex, to the index of the selected item. (We'll use this later in our SelChanged() function.) After getting the index of the selected item, we get the text for the selected item using TListBox::GetSelString().

Now we're ready to create the edit control. We begin by calculating its x, y, width, and height attributes, so we'll know where to put the edit control and how large to make it. TListBox makes this task easy by providing a function called GetItemRect(), which returns the location and size of the currently selected item in the list box.

Now that we have the size of the list box item, we instantiate a TEdit object using the list box as the parent; an arbitrary control ID of 1 (we don't need a meaningful identifier in this example); and the x, y, width, and height attributes of the item rectangle previously obtained with GetItemRect(). We don't want a border on the edit control (we want it to appear invisible), so we remove the WS_BORDER style bit from the Attr.Style member of the edit control.

The next step is to actually create the edit control. (Note that the SetupWindow() function creates child controls or windows for an OWL object. If you create a new child object after you call SetupWindow(), then you need to create the object using the Create() function. OWL will still automatically delete the child object for you.) Following edit->Create(), we take the text we obtained from the list box and set the edit control's text to that string using the TEdit class's function SetText(). Finally, we set the focus to the edit control. If we didn't do this, the edit control would exist onscreen, but the user would have to click it before the editing cursor would be visible.

At this point we have a functioning edit control­­but we need to put the modified text back into the list box. Before we do this, we need an indication that the user has finished editing the selection. We'll use the LBN_SELCHANGE notification for that purpose. When we receive this message, we'll know the user has selected another item in the list box and must be finished editing. (In practice, you'd want to account for the list box closing, certain keystroke combinations, the user pressing the [Enter] key in the edit control, and so on. For this example, though, the LBN_SELCHANGE message will do.)

Next, let's take a look at our SelChanged() function. Remember that our application will call this function when the user clicks a selection in the list box. First we check to see if the edit control is active. If not (edit == NULL), then we return without further action. If the edit control is active, we use GetText() to get the text from the edit control. At this point we're done with the edit control.

Now that we have the modified text, we need to put it in the list box. Unfortunately, Windows doesn't allow us to simply change the text of an existing list box item. As a workaround, we must first delete the item at editIndex (remember, we saved this index earlier), then use TListBox::InsertString() to insert the new text at the same location, as demonstrated in the example. As a last step, we delete the edit pointer and set it to NULL.

We mentioned earlier that OWL objects take care of deleting their children. The same is true in this case. However, we may be creating this edit control many times as the program executes. Therefore, we want to free the memory the edit control uses as soon as we're finished with it. It's safe to delete OWL children yourself­­it's just not usually necessary.

The remaining code sets the main window of the application to a TFrameWindow object using an instance of our EditListBox class as a client window. You'll want to play around with the example program to get a feel for how the technique works.

Try it out

When you run the project, the resulting list box will contain all the lines from your WIN.INI file. When you double-click any item, the item will become an edit control, and a cursor will appear at the left edge of the control. Now you can edit the item as you like. Feel free to modify any of the items in the list box, because we don't actually write the list box back to the WIN.INI file.

Conclusion

As you can see, creating a list box to simulate direct editing is fairly easy. By using a combination of this technique and the owner-drawn list box that we demonstrated last month, you can give your applications unique and professional-looking results with a minimum of effort.

Kent Reisdorph is a professional C++ developer and a member of TeamB, Borland's online support team. You can contact Kent via CompuServe at 75522,1174.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.